iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Build on Google AI

液態玻璃美學:結合 Gemini 與 Antigravity IDE,打造 120FPS、多視窗比例與網格順延推擠佈局的強大商務運算終端系列 第 5

Day 5:【高階商務計算機】字級動態縮放與局部游標!徹底解決多位數爆版與長按連續退格

  • 分享至 

  • xImage
  •  

在昨天的文章中,我們為計算機構築了全天候抗跳動的版面基底,透過 100dvh 動態視口、Safe Area 靈動島避障與純 CSS Apple 原生物理彈簧曲線,徹底解決了手機旋轉與多工分割時的破版痛點。然而,當版面穩固後,使用者的目光隨即聚焦在最核心的文字顯示與點擊體驗上:商務會計需要一眼看清萬億大額的每一位千分位,科學運算則需要緊湊精確的指數表達;當數字過長時,如何既不爆版又能提供最符合直覺的讀取體驗?打錯字時,難道只能把後面的字全部刪除重來?

今天,我們將深入剖析計算機螢幕面板的排版微距動力學——如何結合 大數字雙模引擎(DIRECT 直顯 vs SCI 科學記號)、autoScaleText 動態自適應縮放演算法、縮至 0.48 極限時無縫開啟 iOS 級橫向滾動,以及算式任意字元的局部點擊游標定位插入,打造兼具商務嚴謹與科學靈動的頂級螢幕!


專案正式上線體驗與開源倉庫

線上體驗 Live Demo:https://410355-collab.github.io/Premium-Business-Calculator/
(強烈建議使用 iPhone Safari 或 Android 手機開啟體驗!)
GitHub 開源倉庫:https://github.com/410355-collab/Premium-Business-Calculator
歡迎提出建議與指導
(開發環境:Google Antigravity IDE + Google Gemini 協同開發)


一、技術痛點:行動端 Web 計算機螢幕的「死板」與互動窒息感

要讓 Web 計算機具備如同原生旗艦工具般的頂級操作手感,前端工程師在文字渲染與輸入互動上經常遭遇四大致命難題:

多位數溢位與「粗暴截斷」:

一般固定字級的網頁元素在數字長度超過容器寬度時,若設定 overflow: hidden,後續輸入的新數字會直接消失在視線外;若使用自動折行,又會把數字拆成上下兩截,讓千萬甚至億級的商務財務數值完全無法辨識。

商務千分位與科學極限的「語意衝突」:

商務人士習慣完整展開千分位逗號(例如 1,000,000,000,000)進行核算,但工程與科學場景在面對極大數或極小數時,展開後的長數字會嚴重佔用版面,失去資訊聚焦度。傳統 Web 工具往往只能「二選一」,無法兼顧不同專業場景的需求。

缺乏「局部游標」的重打折磨:

傳統計算機多為「純尾端增刪」,若使用者在輸入了 15 位數的算式後發現第 3 個數字打錯了,傳統 Web 工具逼迫使用者必須連續點擊十幾次退格鍵,把後面打對的數字通通刪除,極度反人性。

千分位逗號引發的「座標偏移錯位」:

商務計算機為了易讀性,數字常帶有動態千分位逗號。若開發者單純用點擊位置換算字串索引,一旦字串因數值跨越千位而自動增加或減少逗號,游標座標就會瞬間產生位移錯位,導致插錯位置。

二、解決方案:大數字雙模引擎、漸進縮放與智慧游標映射

針對上述痛點,我們在架構層面設計了四道核心防禦體系:

大數字雙模顯示篇:DIRECT 直顯 vs SCI 科學記號

於設定選單新增「大數字顯示」選項,提供兩種頂級體驗:

直接顯示:

完整保留超大數值千分位展開,透過 autoScaleText 動態自適應縮放並支援左右橫向拖曳滑動。

科學記號:

當數值為大數(≥ 1e12)或微數(< 1e-6)時,自動調用 math.format 轉為標準緊湊指數型態(如 1.23456789e+15),常規數值則維持千分位,兩相兼顧。
https://ithelp.ithome.com.tw/upload/images/20260908/20183974s0F1NqdOup.png
https://ithelp.ithome.com.tw/upload/images/20260908/20183974c785nAxNZM.png

雙階段漸進縮放篇:縮放貼邊到橫向滾動

設計 autoScaleText 引擎,初始以最大滿版字級呈現。當字串長度超出容器寬度時,第一階段啟動 GPU transform: scale() 動態縮放,將視覺邊界 100% 貼滿整行,不留多餘縫隙;當縮小至物理易讀極限 MIN_SCALE_RATIO (0.48) 時,自動無縫切換至第二階段——解鎖 iOS 級平滑橫向滾動 (scrollLeft),並強制視線追蹤最右側的最新輸入字元。
https://ithelp.ithome.com.tw/upload/images/20260908/201839740CnLIrsgmF.png
https://ithelp.ithome.com.tw/upload/images/20260908/20183974gqAAvyk3X9.png
往右滑後
https://ithelp.ithome.com.tw/upload/images/20260908/20183974GkhNtVXJ9d.png

動態對齊錨點篇:消除左右空白破綻

文字未溢出時,文字靠右對齊 (transform-origin: right center);一旦開始縮放,動態將錨點切換至靠左 (transform-origin: left center),徹底杜絕文字縮小後左右兩側露出空白或被外層容器遮罩吃字的排版破綻。

千分位座標映射篇:動態反解雙向索引

由於商務數字包含動態千分位逗號,我們建立了「原始數值索引」與「格式化字串索引」的雙向映射演算法。無論字串如何動態增減逗號,閃爍游標都能精確落在使用者手指點擊的中段字元之間,支援在任意位置隨插即算。

三、核心代碼精華:大數字雙模格式化與動態縮放引擎 (script.js)

以下是專案中負責大數字模式切換、漸進縮放滾動與游標插入的核心心臟代碼:

/* ─── 1. 大數字雙模格式化引擎 (DIRECT vs SCI) ─── */
function formatNumber(num) {
    if (num === null || num === undefined) return "0";
    if (num === "Error") return "Error";

    // 🔬 科學記號模式:當數值為大數 (|bn| >= 1e12 或非零極小數 |bn| < 1e-6) 時,以標準科學記號格式化
    if (largeNumberFormat === 'scientific') {
        try {
            const bn = math.isBigNumber(num) ? num : math.bignumber(String(num).replace(/,/g, ''));
            if (bn.isFinite()) {
                const absBn = bn.abs();
                if (absBn.gte(math.bignumber('1e12')) || (!absBn.isZero() && absBn.lt(math.bignumber('1e-6')))) {
                    return math.format(bn, { notation: 'exponential', precision: 10 });
                }
            }
        } catch (e) {
            // 例外降級回一般千分位格式
        }
    }

    // 💼 直接顯示模式:完整保留千分位展開,配合 autoScaleText 動態縮放
    const str = stripFloatEpsilon(num);
    if (str === "Error") return "Error";
    const parts = str.split(".");
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    return parts.join(".");
}

/* ─── 2. 動態文字漸進縮放與水平滾動引擎 (autoScaleText) ─── */
const MIN_SCALE_RATIO = 0.48; // 保持清晰可讀的物理極限縮放底線
let batchDisplayRaf = null;

function requestBatchDisplayUpdate() {
    if (batchDisplayRaf) return;
    // 使用 requestAnimationFrame 批次排程,徹底消除 Forced Reflow 佈局抖動
    batchDisplayRaf = requestAnimationFrame(() => {
        batchDisplayRaf = null;
        if (!DOM.resultScaler) DOM.init();

        const items = [
            { s: DOM.resultScaler, w: DOM.resultWrapper, key: 'resultWrapper' },
            { s: DOM.formulaScaler, w: DOM.formulaWrapper, key: 'formulaWrapper' },
            { s: DOM.convertedScaler, w: DOM.convertedWrapper, key: 'convertedWrapper' }
        ];

        // [Read Phase: 集中批次讀取容器與字串真實寬度]
        const measurements = [];
        for (let i = 0; i < items.length; i++) {
            const { s, w, key } = items[i];
            if (!s || !w) continue;
            const containerWidth = cachedWidths[key] || w.clientWidth;
            const contentWidth = s.scrollWidth; // 取得未受限時的真實排版寬度
            const ratio = (contentWidth > containerWidth && containerWidth > 0)
                ? Math.max(containerWidth / contentWidth, MIN_SCALE_RATIO)
                : 1;
            measurements.push({ s, w, ratio, contentWidth, containerWidth });
        }

        // [Write Phase: 集中批次寫入 GPU 變形與滾動位置]
        for (let i = 0; i < measurements.length; i++) {
            const { s, w, ratio, contentWidth, containerWidth } = measurements[i];
            s.dataset.currentRatio = ratio;

            if (ratio < 1) {
                // 縮放時錨點切至左側,精準 100% 貼滿整行,左右零空隙
                s.style.transformOrigin = 'left center';
                s.style.transform = `scale(${ratio}) translate3d(0, 0, 0)`;
            } else {
                s.style.transformOrigin = 'right center';
                s.style.transform = 'scale(1) translate3d(0, 0, 0)';
            }

            const visualContentWidth = contentWidth * ratio;
            // 縮小至極限 0.48 且視覺寬度依然超出時,無縫開啟橫向滾動並緊追最新字元
            if (visualContentWidth > containerWidth + 1 && ratio <= MIN_SCALE_RATIO + 0.001) {
                const maxScroll = Math.max(0, visualContentWidth - containerWidth);
                w.scrollLeft = maxScroll;
            } else {
                w.scrollLeft = 0; // 動態縮放區間強制鎖定零滾動,避免左右晃動
            }
        }
    });
}

/* ─── 3. 局部游標與千分位雙向映射插入邏輯 ─── */
function renderResultWithCursor(formattedInput, currentInput, cursorPos) {
    if (cursorPos === null || cursorPos < 0 || cursorPos > currentInput.length) {
        return formattedInput;
    }
    // 穿透千分位逗號,精準計算格式化字串中閃爍游標的落點位置
    let rawIdx = 0, fmtIdx = 0;
    while (rawIdx < cursorPos && fmtIdx < formattedInput.length) {
        if (formattedInput[fmtIdx] === ',') {
            fmtIdx++;
        } else {
            rawIdx++;
            fmtIdx++;
        }
    }
    const before = formattedInput.slice(0, fmtIdx);
    const after = formattedInput.slice(fmtIdx);
    return `${before}<span class="formula-cursor"></span>${after}`;
}

四、 深度調校剖析:雙模邊界判定與讀寫分離優化

在 Google Antigravity IDE 的除錯過程中,這套螢幕顯示機制展現了強大的精準度與效能優勢:

指數觸發閾值的「黃金界限」:

在實作科學記號時,若門檻設得太低(如 1e6),一般百萬級商務金額就會被轉為科學記號,失去閱讀習慣;若門檻太高,又失去緊湊展示的意義。我們設定 ≥ 1e12(兆級以上)與 < 1e-6(微數以下)作為自動格式化分水嶺,既尊重了常規商務對帳,又為天文級計算保留了簡約清晰的視圖。

讀寫分離的「零卡頓」:

傳統在每次按鍵輸入時,如果一邊讀取 clientWidth、一邊立刻設定 style.transform,瀏覽器會被迫進入「強制同步排版 (Forced Reflow)」,連續快速點擊數字鍵時幀率會暴跌。我們透過 requestAnimationFrame 將所有量測集中在第一階段,樣式套用集中在第二階段,即使在低階手機上高速連敲也能維持滿幀流暢。

游標定位的「不跳格」:

在數字中段插入字元時,數字可能突然跨越千位門檻(例如 999 變成 1,000,字串長度因逗號額外多出 1 碼)。若單純用下標定位,游標會瞬間被推擠跳位。透過雙索引循環解構,游標永遠咬死在使用者手指指定的真實字元後方,輸入體驗如絲般順滑。

五、總結

商務與科學兼備的數字顯示:

「直接顯示」與「科學記號」雙模切換讓計算機能在百億商務流水帳與天文微觀算式之間自由穿梭,搭配 autoScaleText 達成真正的自適應滿版。

媲美原生系統的精準游標操作:

穿透千分位逗號的雙向游標映射,讓使用者能隨時隨地在算式中間精準插入或修改數字,免去全數刪除重打的困擾。

明日預告:仿 iOS 桌面拖曳重排!順延推擠 (Push & Reflow) 演算法與槽位指示器

螢幕顯示與長算式排版完美就緒了,但每個人常用的計算功能截然不同——做生意的需要稅率與折扣鍵,念工程的需要三角函數與次方鍵。如何讓使用者像在 iPhone 桌面整理 App 一樣,隨意拖曳按鍵調換順序,而且其他按鍵還會即時優雅地往旁邊「讓出空位」?
在接下來幾天中,我們將深入探索:如何用純 GPU 硬體加速打造無卡頓的按鍵順延推擠 (Push & Reflow) 演算法,以及 100% 絕對視口座標貼合的發光槽位指示器!


上一篇
Day 4:【高階商務計算機】多工分割與旋轉零破版!使用用彈簧曲線打造滿幀服貼佈局
下一篇
Day 6:【高階商務計算機】仿行動裝置桌面拖曳重排!順延推擠挪位演算法與高幀率排程
系列文
液態玻璃美學:結合 Gemini 與 Antigravity IDE,打造 120FPS、多視窗比例與網格順延推擠佈局的強大商務運算終端7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言